package pl.tssoft.math.core.parser;

import java.util.LinkedList;
import java.util.List;
import pl.tssoft.math.core.Value;
import pl.tssoft.math.core.calculation.Calculable;
import pl.tssoft.math.core.calculation.objects.CalculableList;
import pl.tssoft.math.core.calculation.objects.FunctionObject;
import pl.tssoft.math.core.calculation.objects.SquareBlockObject;
import pl.tssoft.math.core.calculation.objects.ValueObject;
import pl.tssoft.math.core.calculation.objects.VariableObject;
import pl.tssoft.math.core.calculation.operator.Operator;
import pl.tssoft.math.core.calculation.operator.builtin.special.EndingOperator;
import pl.tssoft.math.core.exceptions.ErrorCode;
import pl.tssoft.math.core.exceptions.SyntaxException;
import static pl.tssoft.math.core.parser.ElementType.*;

/**
 *
 * @author tserafin
 */
public class Parser {

    //todo return type
    public static void parse(String multiExpression) throws SyntaxException {

        List<Calculable> calculables = new LinkedList<Calculable>();
        InputIterator input = new InputIterator(multiExpression);
        ParserConfig config = new ParserConfig().withOmitWhitespaces(true).withExpressionEndingCharacters(",;");

        while (input.isPresent()) {
            Calculable c = parseExpression(input, config).result;
            calculables.add(c);
        }

        for (Calculable c : calculables) {
            //todo
        }
    }

    static ParseExpressionResult parseExpression(InputIterator input, ParserConfig config) throws SyntaxException {

        CalculableList cl = new CalculableList();
        ParserContext context = new ParserContext();
        context.previousElementType = BEGIN;

        ParseExpressionResult result = new ParseExpressionResult(input.getPosition());

        while (true) {

            context.currentElementPosibbleMinPosition = input.getPosition();
            if (config.omitWhitespaces) {
                input.omitWhitespaces();
            }
            if (input.isPresent()) {
                if (context.previousElementType == BEGIN) {
                    result.expressionStartPosition = input.getPosition();
                }
                context.currentElementPosition = input.getPosition();
            }

            Calculable calculable = null;

            context.currentElementType = ParserUtil.getCurrentElementType(input);
            if (context.currentElementType == OPERATOR_OR_END_OF_EXPRESSION) {
                Operator operator = ParserUtil.parseOperator(input, config);
                context.currentElementType = operator instanceof EndingOperator ? OPERATOR_END_OF_EXPRESSION : ElementType.valueOf(operator.getType());
                calculable = operator;
            }

            checkCurrentElementType(context);

            switch (context.currentElementType) {
                case NAME:
                    calculable = parseVariableOrFunction(input, config);
                    break;

                case NUMBER:
                    Value value = ParserUtil.parseNumber(input);
                    calculable = new ValueObject(value);
                    break;

                case BRACKET_BLOCK:
                    calculable = parseBracketBlock(input);
                    break;

                case SQUARE_BLOCK:
                    calculable = parseSquareBlock(input);
                    break;

                case OPERATOR_END_OF_EXPRESSION:
                    cl.setShowResult(((EndingOperator) calculable).showResult());
                    result.expressionEndPosition = input.getPreviousNonWhitespacePosition();
                    result.result = cl;
                    return result;

                case OPERATOR_RIGHT_SIDE:
                case OPERATOR_LEFT_SIDE:
                case OPERATOR_LEFT_OR_BOTH_SIDE:
                case OPERATOR_BOTH_SIDE:
                    break;

                default:
                    throw new AssertionError();
            }

            context.previousElementType = context.currentElementType;
            cl.getList().add(calculable);
        }
    }

    private static void checkCurrentElementType(ParserContext context) throws SyntaxException {
        ElementType current = context.currentElementType;
        ElementType previous = context.previousElementType;
        int currentPosition = context.currentElementPosition;
        switch (previous) {
            case BEGIN:
                if (current == OPERATOR_BOTH_SIDE || current == OPERATOR_RIGHT_SIDE) {
                    throw new SyntaxException(currentPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
                }
                break;

            case NUMBER:
            case OPERATOR_RIGHT_SIDE:
            case SQUARE_BLOCK:
            case BRACKET_BLOCK:
                if (!current.isOperatorSupportingBothSide() && !current.isOperatorSupportingRightSide()
                        && current != OPERATOR_END_OF_EXPRESSION) {
                    throw new SyntaxException(currentPosition, ErrorCode.ERR_EXPECTED_OPERATOR);
                }
                break;

            case NAME:
                if (!current.isOperatorSupportingBothSide() && !current.isOperatorSupportingRightSide()
                        && current != OPERATOR_END_OF_EXPRESSION && current != BRACKET_BLOCK) {
                    throw new SyntaxException(currentPosition, ErrorCode.ERR_EXPECTED_OPERATOR);
                }
                break;

            case OPERATOR_LEFT_SIDE:
            case OPERATOR_LEFT_OR_BOTH_SIDE:
            case OPERATOR_BOTH_SIDE:
                if (!current.isSquareOrBracketBlock() && !current.isNameOrNumber() && !current.isOperatorSupportingLeftSide()) {
                    throw new SyntaxException(context.currentElementPosibbleMinPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
                }
                break;

            default:
                throw new AssertionError(previous.name());
        }
    }

    private static Calculable parseVariableOrFunction(InputIterator input, ParserConfig config) throws SyntaxException {
        int startPosition = input.getPosition();
        String name = ParserUtil.readName(input);
        if (config.omitWhitespaces) {
            input.omitWhitespaces();
        }
        Character c = input.get();
        if (!c.equals('(')) {
            return new VariableObject(name, startPosition);
        }

        //todo check if function exists

        List<Calculable> params = parseFunctionParameters(input);
        return new FunctionObject(name, startPosition, params);
    }

    /**
     * Parses function parameters list closed in (), e.g.
     * <pre>
     *     (1, sin(x), 2 -4)    3 parameters
     *     (x)                  1 parameter
     *     ()                   0 parameters
     * </pre> Parsing examples below ends with exception
     * <pre>
     *     (,12)
     *     (12,)
     * </pre> This methods assumes that input points to '(' character
     */
    private static List<Calculable> parseFunctionParameters(InputIterator input) throws SyntaxException {
        List<Calculable> params = new LinkedList<Calculable>();

        ParserConfig config = new ParserConfig().withOmitWhitespaces(true).withExpressionEndingCharacters(",)");

        ParserUtil.assertInputPointsCharacterAndForward(input, '(');

        boolean first = true;
        while (true) {
            ParseExpressionResult result = parseExpression(input, config);
            CalculableList param = result.result;
            char prevChar = input.getPrevious();
            if (param.isEmpty()) {
                if (first) {
                    if (prevChar == ')') {
                        return params;
                    }
                    if (prevChar == ',') {
                        throw new SyntaxException(input.getPosition() - 1, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
                    }
                    if (!Character.isWhitespace(prevChar) && result.expressionEndPosition > result.expressionMinPossiblePosition) {
                        throw new SyntaxException(result.expressionStartPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
                    }
                    throw new SyntaxException(result.expressionStartPosition, ErrorCode.ERR_EXPECTED_CLOSING_BRACKET);
                }
                throw new SyntaxException(result.expressionStartPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
            }
            first = false;

            params.add(param);
            if (prevChar == ')') {
                return params;
            }

            if (prevChar != ',') {
                throw new SyntaxException(result.expressionEndPosition + 1, ErrorCode.ERR_EXPECTED_CLOSING_BRACKET);
            }
        }
    }

    private static Calculable parseBracketBlock(InputIterator input) throws SyntaxException {
        ParserUtil.assertInputPointsCharacterAndForward(input, '(');

        ParserConfig config = new ParserConfig().withOmitWhitespaces(true).withExpressionEndingCharacters(")");
        ParseExpressionResult result = parseExpression(input, config);
        CalculableList calculable = result.result;

        if (calculable.isEmpty()) {
            throw new SyntaxException(result.expressionStartPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
        }

        char prevChar = input.getPrevious();
        if (prevChar != ')') {
            throw new SyntaxException(result.expressionEndPosition + 1, ErrorCode.ERR_EXPECTED_CLOSING_BRACKET);
        }

        return calculable;
    }

    private static Calculable parseSquareBlock(InputIterator input) throws SyntaxException {
        ParserUtil.assertInputPointsCharacterAndForward(input, '[');
        input.omitWhitespaces();

        List<List<Calculable>> params = new LinkedList<List<Calculable>>();
        //todo name
        SquareBlockObject object = new SquareBlockObject(params);

        List<Calculable> currentRow = new LinkedList<Calculable>();
        params.add(currentRow);

        ParserConfig config = new ParserConfig().withOmitWhitespaces(false).withExpressionEndingCharacters("] ,;");


        while (true) {
            ParseExpressionResult result = parseExpression(input, config);
            char prevChar = input.getPrevious();

            CalculableList param = result.result;
            if (param.isEmpty()) {
                throw new SyntaxException(result.expressionStartPosition, ErrorCode.ERR_EXPECTED_VARIABLE_OR_FUNCTION);
            }

            currentRow.add(param);
            if (prevChar == ']') {
                return object;
            }

            if (prevChar != ',' && prevChar != ';' && prevChar != ' ') {
                throw new SyntaxException(result.expressionEndPosition + 1, ErrorCode.ERR_EXPECTED_CLOSING_SQUARE_BRACKET);
            }

            if (prevChar == ' ') {      //e.g. [1  ,3] or [ 2   ;3]
                input.omitWhitespaces();
                char c = input.get();
                if (input.isPresent() && (c == ';' || c == ',' || c == ']')) {
                    input.forward();
                }
                prevChar = input.getPrevious();
                if (prevChar == ']') {
                    return object;
                }
            }

            if (prevChar == ';') {
                currentRow = new LinkedList<Calculable>();
                params.add(currentRow);
            }

            input.omitWhitespaces();
        }
    }
}
